package fr.norsys.mapper.console.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.naming.Context;

import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.Unmarshaller;
import org.xml.sax.InputSource;

import fr.norsys.mapper.console.exception.FileException;
import fr.norsys.mapper.console.exception.MappingException;
import fr.norsys.mapper.console.mapping.Add;
import fr.norsys.mapper.console.mapping.Config;
import fr.norsys.mapper.console.mapping.Delete;
import fr.norsys.mapper.console.mapping.Map;
import fr.norsys.mapper.console.mapping.Mapper;
import fr.norsys.mapper.console.mapping.MapperConfig;
import fr.norsys.mapper.console.mapping.Modify;
import fr.norsys.mapper.console.mapping.Property;
import fr.norsys.mapper.console.mapping.RegExp;
import fr.norsys.mapper.console.mapping.Search;
import fr.norsys.mapper.console.mapping.Variable;
import fr.norsys.mapper.console.model.Application;
import fr.norsys.mapper.console.model.Attribute;
import fr.norsys.mapper.console.model.Connection;
import fr.norsys.mapper.console.model.ConsoleCst;
import fr.norsys.mapper.console.model.Resource;
import fr.norsys.mapper.console.utils.BeanUtils;
import fr.norsys.mapper.console.utils.UIDGenerator;

public class MappingServiceImpl implements MappingService {
	private final static Log LOG = LogFactory.getLog(MappingServiceImpl.class);
	private String mappingFile;

	public String getMappingFile() {
		return mappingFile;
	}

	public void setMappingFile(String mappingFile) {
		this.mappingFile = mappingFile;
	}
	public void importApplicationConfig(Application application, InputStream is) throws MappingException {
		String applicationName = application.getName();
		application.clear();
		Validate.notNull("applicationName must not be null", applicationName);
		Validate.notEmpty("applicationName must not be empty", applicationName);
		MapperConfig mc = (MapperConfig) unmarshall(is);
		if (mc == null){
			application.setConnection(new Connection());
			application
					.setConnectionName(application.getConnection().getName());
		}
		else {
			fillApplication(application, mc);
			fillConnection(application, mc.getJndiMapper().getSource()
					.getConfig().getProperties());
		}
	}
	public void loadMapping(Application application, String mapFile) throws MappingException {
		String applicationName = application.getName();
		application.clear();
		Validate.notNull("applicationName must not be null", applicationName);
		Validate.notEmpty("applicationName must not be empty", applicationName);
		MapperConfig mc = (MapperConfig) unmarshall(mapFile);
		if (mc == null){
			application.setConnection(new Connection());
			application
					.setConnectionName(application.getConnection().getName());
		}
		else {
			fillApplication(application, mc);
			fillConnection(application, mc.getJndiMapper().getSource()
					.getConfig().getProperties());
		}
	}

	private boolean isVariable(String val) {
		boolean result = false;
		if (!"".equals(val) && val.startsWith("${") && val.endsWith("}"))
			{result = true;}
		return result;
	}

	private String getValue(String val, Collection variables) {
		String result = val;
		if (val != null && !"".equals(val) && variables != null && isVariable(val)) {
			for (Iterator it = variables.iterator(); it.hasNext();) {
				fr.norsys.mapper.console.model.Variable v = (fr.norsys.mapper.console.model.Variable) it.next();
				if (val.substring(2, val.length() - 1).equals(v.getName())) {
					result = v.getValue();
				}
			}			
		}
		return result;
	}

	private void fillConnection(Application application, Vector properties) {
		Connection connection = new Connection();
		connection.setId(UIDGenerator.generateId());
		for (Iterator it = properties.iterator(); it.hasNext();) {
			Property property = (Property) it.next();
			if (Context.PROVIDER_URL.equals(property.getKey())) {
				connection.setUrl(getValue(property.getVal(), application
						.getVariables()));
			} else if (Context.SECURITY_PRINCIPAL.equals(property.getKey())) {
				connection.setUser(getValue(property.getVal(), application
						.getVariables()));
			} else if (Context.SECURITY_CREDENTIALS.equals(property.getKey())) {
				connection.setPass(getValue(property.getVal(), application
						.getVariables()));
			} else if ("jndi.connection.pool.minPoolSize".equals(property
					.getKey())) {
				connection.setMinPoolSize(getValue(property.getVal(),
						application.getVariables()));
			} else if ("jndi.connection.pool.maxPoolSize".equals(property
					.getKey())) {
				connection.setMaxPoolSize(getValue(property.getVal(),
						application.getVariables()));
			} else if ("jndi.connection.pool.timeout".equals(property.getKey())) {
				connection.setTimeoutPool(getValue(property.getVal(),
						application.getVariables()));
			} else if ("jndi.connection.name".equals(property.getKey())) {
				connection.setName(getValue(property.getVal(), application
						.getVariables()));
			}
		}
		application.setConnection(connection);
		application.setConnectionName(connection.getName());
	}

	private void fillVariables(Application application, Vector variables) {
		for (Iterator it = variables.iterator(); it.hasNext();) {
			Variable v = (Variable) it.next();
			application
					.addVariable(new fr.norsys.mapper.console.model.Variable(v
							.getName(), v.getValue()));
		}
	}

	private void fillInAttributes(Resource r, Vector attributes, Vector regexps) {
		for (Iterator it = attributes.iterator(); it.hasNext();) {
			fr.norsys.mapper.console.mapping.Attribute a = (fr.norsys.mapper.console.mapping.Attribute) it
					.next();
			if(LOG.isDebugEnabled()) {
				LOG.debug("Entry attribute mapped: " + a.getKey());
			}
			Attribute attribute = new Attribute(a.getKey());
			attribute.setAttributeLDAP(a.getVal());
			attribute.setDefaultValue(a.getVal());
			attribute.setInputOutput(ConsoleCst.INPUT_ATTRIBUTE_TYPE);
			for (Iterator it2 = regexps.iterator(); it2.hasNext();) {
				RegExp re = (RegExp) it2.next();
				if (a.getKey().equals(re.getKey())) {
					attribute.setRule(re.getValue());
					attribute.setIgnoreNull(re.getIgnoreNull());
				}
			}
			r.addAttribute(attribute);
		}
	}

	private Attribute hasSymetric(Resource r, String key, String val) {
		Attribute result = null;
		for (Iterator it = r.getAttributes().iterator(); it
				.hasNext();) {
			Attribute attr = (Attribute) it.next();
			if ( val.equals(attr.getName())
					&& ConsoleCst.INPUT_ATTRIBUTE_TYPE.equals(attr
							.getInputOutput())){
				{result = attr;}
				result.setAttributeLDAP(key);				
			}
		}
		return result;
	}

	private void fillOutAttributes(Resource r, Vector attributes, Vector regexps) {
		for (Iterator it = attributes.iterator(); it.hasNext();) {
			fr.norsys.mapper.console.mapping.Attribute a = (fr.norsys.mapper.console.mapping.Attribute) it
					.next();
			if(LOG.isDebugEnabled()) {
				LOG.debug("Out attribute mapped: " + a.getKey());
			}
			Attribute symetric = hasSymetric(r, a.getKey(), a.getVal());
			if (symetric == null){
				Attribute attribute = new Attribute(a.getVal());
				attribute.setAttributeLDAP(a.getKey());
				attribute.setInputOutput(ConsoleCst.OUTPUT_ATTRIBUTE_TYPE);
				for (Iterator it2 = regexps.iterator(); it2.hasNext();) {
					RegExp re = (RegExp) it2.next();
					if (a.getKey().equals(re.getKey())) {
						attribute.setRule(re.getValue());
						attribute.setIgnoreNull(re.getIgnoreNull());
					}
				}
				r.addAttribute(attribute);
			} else {
				symetric.setInputOutput(ConsoleCst.INPUT_OUTPUT_ATTRIBUTE_TYPE);	
				r.addAttribute(symetric);
			}
		}
	}

	private void fillSearch(Application application, Vector searchList,
			Vector maps, Vector regexps) {
		for (Iterator it = searchList.iterator(); it.hasNext();) {
			Search s = (Search) it.next();
			if(LOG.isDebugEnabled()) {
				LOG.debug("Search mapper mapped: " + s.getName());
			}
			regexps.addAll(s.getRegexps());
			Resource r = new Resource(s.getName(), s.getRoot(), null,
					ConsoleCst.SEARCH_MAPPER_TYPE, s.getFilter(), s
							.getSearchScope());
			Vector inputAttr = new Vector();
			Vector outputAttr = new Vector();
			for (Iterator it2 = maps.iterator(); it2.hasNext();) {
				Map m = (Map) it2.next();
				if (s.getInput().equals(m.getName())) {
					inputAttr = m.getAttributes();
				} else if (s.getOutput().equals(m.getName())) {
					outputAttr = m.getAttributes();
				}
			}
			fillInAttributes(r, inputAttr, regexps);
			fillOutAttributes(r, outputAttr, regexps);

			application.addResource(r);
		}
	}

	private void fillAdd(Application application, Vector addList, Vector maps,
			Vector regexps) {
		for (Iterator it = addList.iterator(); it.hasNext();) {
			Add a = (Add) it.next();
			if(LOG.isDebugEnabled()) {
				LOG.debug("Add mapper mapped: " + a.getName());
			}
			regexps.addAll(a.getRegexps());
			Resource r = new Resource(a.getName(), getBaseDn(a.getRoot()), getIdentifiant(a.getRoot()),
					ConsoleCst.ADD_MAPPER_TYPE, null, null);
			Vector inputAttr = new Vector();
			Vector outputAttr = new Vector();
			for (Iterator it2 = maps.iterator(); it2.hasNext();) {
				Map m = (Map) it2.next();
				if (a.getInput().equals(m.getName())) {
					inputAttr = m.getAttributes();
				} else if (a.getOutput().equals(m.getName())) {
					outputAttr = m.getAttributes();
				}
			}
			fillInAttributes(r, inputAttr, regexps);
			fillOutAttributes(r, outputAttr, regexps);

			application.addResource(r);
		}
	}



	private void fillDelete(Application application, Vector deleteList,
			Vector maps, Vector regexps) {
		for (Iterator it = deleteList.iterator(); it.hasNext();) {
			Delete d = (Delete) it.next();
			if(LOG.isDebugEnabled()) {
				LOG.debug("Delete mapper mapped: " + d.getName());
			}
			regexps.addAll(d.getRegexps());
			Resource r = new Resource(d.getName(), getBaseDn(d.getRoot()), getIdentifiant(d.getRoot()),
					ConsoleCst.DELETE_MAPPER_TYPE, null, null);
			Vector inputAttr = new Vector();
			Vector outputAttr = new Vector();
			for (Iterator it2 = maps.iterator(); it2.hasNext();) {
				Map m = (Map) it2.next();
				if (d.getInput().equals(m.getName())) {
					inputAttr = m.getAttributes();
				} else if (d.getOutput().equals(m.getName())) {
					outputAttr = m.getAttributes();
				}
			}
			fillInAttributes(r, inputAttr, regexps);
			fillOutAttributes(r, outputAttr, regexps);

			application.addResource(r);
		}
	}

	private void fillModify(Application application, Vector modifyList,
			Vector maps, Vector regexps) {
		for (Iterator it = modifyList.iterator(); it.hasNext();) {
			Modify m1 = (Modify) it.next();
			if(LOG.isDebugEnabled()) {
				LOG.debug("Modify mapper mapped: " + m1.getName());
			}
			regexps.addAll(m1.getRegexps());
			Resource r = new Resource(m1.getName(), getBaseDn(m1.getRoot()), getIdentifiant(m1.getRoot()),
					ConsoleCst.MODIFY_MAPPER_TYPE, null, null);
			Vector inputAttr = new Vector();
			Vector outputAttr = new Vector();
			for (Iterator it2 = maps.iterator(); it2.hasNext();) {
				Map m = (Map) it2.next();
				if (m1.getInput().equals(m.getName())) {
					inputAttr = m.getAttributes();
				} else if (m1.getOutput().equals(m.getName())) {
					outputAttr = m.getAttributes();
				}
			}
			fillInAttributes(r, inputAttr, regexps);
			fillOutAttributes(r, outputAttr, regexps);

			application.addResource(r);
		}
	}

	private void fillApplication(Application application, MapperConfig mc) {
		if(LOG.isDebugEnabled()) {
			LOG.debug("Application mapped: " + application.getName());
		}
		fillVariables(application, mc.getVariables());
		fillSearch(application, mc.getJndiMapper().getSource().getSearchList(),
				mc.getJndiMapper().getMaps(), mc.getJndiMapper().getRegexps());
		fillAdd(application, mc.getJndiMapper().getSource().getAddList(), mc
				.getJndiMapper().getMaps(), mc.getJndiMapper().getRegexps());
		fillDelete(application, mc.getJndiMapper().getSource().getDeleteList(),
				mc.getJndiMapper().getMaps(), mc.getJndiMapper().getRegexps());
		fillModify(application, mc.getJndiMapper().getSource().getModifyList(),
				mc.getJndiMapper().getMaps(), mc.getJndiMapper().getRegexps());
	}

	public void saveMapping(Application application, String mapFile) throws MappingException {
		MapperConfig mapperConfig = new MapperConfig();
		java.util.Collection variables = application.getVariables();
		for (Iterator it = variables.iterator(); it.hasNext();) {
			Variable variable = new Variable();
			fr.norsys.mapper.console.model.Variable v = (fr.norsys.mapper.console.model.Variable)it.next();
			BeanUtils.copyFilledProperties(variable, v);
			mapperConfig.addVariable(variable);
		}

		Connection connection = application.getConnection();
		mapperConfig.getJndiMapper().getSource().setName(connection.getName());
		Config config = mapperConfig.getJndiMapper().getSource().getConfig();
		config.getProperties().add(
				new Property(Context.INITIAL_CONTEXT_FACTORY,
						"com.sun.jndi.ldap.LdapCtxFactory"));
		config.getProperties().add(
				new Property(Context.PROVIDER_URL, connection.getUrl()));
		config.getProperties().add(
				new Property(Context.SECURITY_PRINCIPAL, connection.getUser()));
		config.getProperties()
				.add(
						new Property(Context.SECURITY_CREDENTIALS, connection
								.getPass()));
		config.getProperties().add(
				new Property("jndi.connection.pool.minPoolSize", connection
						.getMinPoolSize()));
		config.getProperties().add(
				new Property("jndi.connection.pool.maxPoolSize", connection
						.getMaxPoolSize()));
		config.getProperties().add(
				new Property("jndi.connection.pool.timeout", connection
						.getTimeoutPool()));
		config.getProperties().add(
				new Property("jndi.connection.name", connection.getName()));

		mapperConfig.getSubConfig().setName(
				"http://norsys.fr/framework-ldap/jndi-configuration.dtd");
		mapperConfig.getSubConfig().setClasse(
				"fr.norsys.mapper.jndi.JNDIXMLConfigurator");

		Mapper mapper = null;
		for (Iterator it = application.getResources().iterator(); it
				.hasNext();) {
			Resource r = (Resource) (it.next());
			String name = r.getName();
			StringBuffer root = new StringBuffer();
			Map inputMap = new Map("input-" + name);
			Map outputMap = new Map("output-" + name);
			if(r.getIdentifiant()!=null && r.getIdentifiant().length()>0){
				root.append(r.getIdentifiant()).append(",");
			}
			root.append(r.getBaseDn());
			if (ConsoleCst.SEARCH_MAPPER_TYPE.equals(r.getType())) {
				mapper = new Search(name, "input-" + name, "output-" + name, r
						.getFilter(), root.toString(), r.getScope(),
						"true", "true", r.getCountLimit());
				mapperConfig.getJndiMapper().getSource().getSearchList().add(mapper);
			} else if (ConsoleCst.ADD_MAPPER_TYPE.equals(r.getType())) {
				mapper = new Add(name, "input-" + name, "output-" + name, root.toString()); 
				mapperConfig.getJndiMapper().getSource().getAddList().add(mapper);
			} else if (ConsoleCst.DELETE_MAPPER_TYPE.equals(r.getType())) {
				mapper = new Delete(name, "input-" + name, "output-" + name, root.toString());
				mapperConfig.getJndiMapper().getSource().getDeleteList().add(mapper);
			} else if (ConsoleCst.MODIFY_MAPPER_TYPE.equals(r.getType())) {
				mapper = new Modify(name, "input-" + name, "output-" + name, root.toString());
				mapperConfig.getJndiMapper().getSource().getModifyList().add(mapper);
			}
			for (Iterator it2 = r.getAttributes().iterator(); it2
					.hasNext();) {
				Attribute a = (Attribute)it2.next();
				if(a.getRule() != null && !"".equals(a.getRule())) {
					mapper.getRegexps().add(new RegExp(a.getName(),a.getRule(),a.getIgnoreNull()));
				}
				String type = a.getInputOutput();
				if (ConsoleCst.INPUT_ATTRIBUTE_TYPE.equals(type)) {
					inputMap.getAttributes().add(
							new fr.norsys.mapper.console.mapping.Attribute(a
									.getName(), a.getDefaultValue()));
				} else if (ConsoleCst.OUTPUT_ATTRIBUTE_TYPE.equals(type)) {
					outputMap.getAttributes().add(
							new fr.norsys.mapper.console.mapping.Attribute(a
									.getAttributeLDAP(), a.getName()));
				} else if (ConsoleCst.INPUT_OUTPUT_ATTRIBUTE_TYPE.equals(type)) {
					inputMap.getAttributes().add(
							new fr.norsys.mapper.console.mapping.Attribute(a
									.getName(), a.getDefaultValue()));
					outputMap.getAttributes().add(
							new fr.norsys.mapper.console.mapping.Attribute(a
									.getAttributeLDAP(), a.getName()));
				}
			}
			mapperConfig.getJndiMapper().getMaps().add(inputMap);
			mapperConfig.getJndiMapper().getMaps().add(outputMap);
		}

		marshall(mapperConfig, mapFile);
	}

	public Object unmarshall(InputStream input) throws MappingException {
		Object obj = null;
		try {
			Mapping mapping = new Mapping();
			URL mappingUrl = Thread.currentThread().getContextClassLoader()
					.getResource(mappingFile);
			mapping.loadMapping(mappingUrl);
			Unmarshaller unmar = new Unmarshaller(mapping);
			if (input != null) {
				obj = unmar.unmarshal(new InputSource(input));
			}
		} catch (Throwable th) {
			throw new MappingException("Unmarshall exception occured: "+th);
		}
		return obj;
	}
	public Object unmarshall(String inputFile) throws MappingException {
		Object obj = null;
		if(inputFile==null)
			{throw new MappingException("File " + inputFile + " doesn't exist");}
		File f = new File(inputFile);
		if(f == null || !f.isFile())
			{throw new MappingException("File " + inputFile + " doesn't exist");}
		try {
			Mapping mapping = new Mapping();
			URL mappingUrl = Thread.currentThread().getContextClassLoader()
					.getResource(mappingFile);
			mapping.loadMapping(mappingUrl);
			Unmarshaller unmar = new Unmarshaller(mapping);
			InputStream input = new FileInputStream(inputFile);
			obj = unmar.unmarshal(new InputSource(input));
			input.close();
		} catch (Throwable th) {
			throw new MappingException("Unmarshall exception occured: "+th);
		}
		return obj;
	}

	public void marshall(Object obj, String outputFile)
			throws MappingException {
		File f = new File(outputFile);
		File dir = f.getParentFile();
		if(f==null || !dir.isDirectory())
			{throw new MappingException("Parent directory of file " + outputFile + " doesn't exist");}
		if(!(obj instanceof MapperConfig))
			{throw new MappingException("Object must be an instance of MapperConfig class");}
		try {
			Mapping mapping = new Mapping();
			URL mappingUrl = Thread.currentThread().getContextClassLoader()
					.getResource(mappingFile);
			mapping.loadMapping(mappingUrl);
			FileWriter writer = new FileWriter(outputFile);
			Marshaller marshaller = new Marshaller(writer);
			marshaller.setEncoding(ConsoleCst.MAPPING_ENCODING);
			marshaller.setMapping(mapping);
			marshaller.marshal(obj);
		} catch (Throwable th) {
			throw new MappingException("Marshall exception occured: "+th);
		}
	}
	
	/**
	 * Service to get the identifiant from the root
	 * @param root
	 * @return
	 */
	public String getIdentifiant(String root) {
		String identifiant = "";
		if(root!=null) {
			List elements = split(root,",");
			if(elements.size() == 1)
				return "";
			List modifiedElements = new ArrayList();
			for(Iterator it=elements.iterator();it.hasNext();) {
				String e = ((String)(it.next())).trim();
				int index = e.indexOf("=");
				if(index > 0) {
					String subEl = e.substring(0,index);
					if(!subEl.equals("ou") && !subEl.equals("o")) {
						modifiedElements.add(e);
					}
				}
			}
			String[] identifiants = (String[]) modifiedElements.toArray(new String[modifiedElements.size()]);
			if(identifiants.length > 0) {
				identifiant = identifiants[0];
				for(int i=1;i<identifiants.length;i++) {
					identifiant += ","+identifiants[i];
				}
			}
		}
		return identifiant;
/*		String result;
		int identifiantIndex = root.lastIndexOf(",ou=");
		if(identifiantIndex>0)
			{result = root.substring(0,identifiantIndex);}
		else {
			identifiantIndex = root.lastIndexOf(",o=");
			if(identifiantIndex>0)
				{result = root.substring(0,identifiantIndex);}
			else
				{result = "";}
		}
		return result;*/
	}

	/**
	 * Service to get the BAseDn from the root
	 * @param root
	 * @return
	 */
	public String getBaseDn(String root) {
		String baseDn = "";
		if(root!=null) {
			List elements = split(root,",");
			if(elements.size() == 1)
				return root;
			List modifiedElements = new ArrayList();
			for(Iterator it=elements.iterator();it.hasNext();) {
				String e = ((String)(it.next())).trim();
				int index = e.indexOf("=");
				if(index > 0) {
					String subEl = e.substring(0,index);
					if(subEl.equals("ou") || subEl.equals("o")) {
						modifiedElements.add(e);
					}
				}
			}
			String[] baseDns = (String[]) modifiedElements.toArray(new String[modifiedElements.size()]);
			if(baseDns.length > 0) {
				baseDn = baseDns[0];
				for(int i=1;i<baseDns.length;i++) {
					baseDn += ","+baseDns[i];
				}
			}
		}
		return baseDn;
		
/*		String result;
		int identifiantIndex = root.lastIndexOf(",ou=");
		if(identifiantIndex>0)
			{result = root.substring(identifiantIndex+1);}
		else {
			identifiantIndex = root.lastIndexOf(",o=");
			if(identifiantIndex>0)
				{result = root.substring(identifiantIndex+1);}
			else
				{result = root;}
		}	
		return result;*/
	}
	private List split(String str,String delimiter) {
		List l = new ArrayList();
		StringTokenizer st = new StringTokenizer(str,delimiter);
		while(st.hasMoreTokens()) {
			l.add(st.nextToken());
		}
		return l;
	}
	/**
	 * Service to load an xml file to export
	 * @param name : name of the application to export
	 * @param path : the temporary location, to popup the file
	 * @return 
	 * @throws FileException
	 */
	public String exportApplicationConfig(String name, String path) throws MappingException {
		File file = new File(path+name);
		BufferedReader reader = null;
		StringBuffer sb = new StringBuffer();
		try {
			reader=  new BufferedReader(new FileReader(file));
			String line = reader.readLine();
			while (line!=null){
				sb.append(line).append("\n");
				line=reader.readLine();
			}
		} catch (Throwable th) {
			throw new MappingException("Exception occured when reading file "+path+file+": "+th);
		} finally {
			if(reader!=null){
				try {
					reader.close();
				} catch (IOException e) {
				}
			}
		}
		return sb.toString();
	}
}